{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "c6a2bd60",
   "metadata": {},
   "source": [
    "# FTTransformer，一个很能打的模型"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "382eb806",
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "source": [
    "FTTransformer是一个可以用于结构化(tabular)数据的分类和回归任务的模型。\n",
    "\n",
    "FT 即 Feature Tokenizer的意思，把结构化数据中的离散特征和连续特征都像单词一样编码成一个向量。\n",
    "\n",
    "从而可以像对text数据那样 应用 Transformer对 Tabular数据进行特征抽取。\n",
    "\n",
    "值得注意的是，它对Transformer作了一些微妙的改动以适应 Tabular数据。\n",
    "\n",
    "例如：去除第一个Transformer输入的LayerNorm层，仿照BERT的设计增加了output token(CLS token) 与features token 一起进行进入Transformer参与注意力计算。\n",
    "\n",
    "参考: https://paperswithcode.com/method/ft-transformer\n",
    "\n",
    "本范例我们使用Covertype数据集，它的目标是预测植被覆盖类型，是一个七分类问题。\n",
    "\n",
    "公众号**算法美食屋**后台回复关键词：torchkeras，获取本文notebook源码和所用Covertype数据集下载链接。\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "79c9440c-4e00-4d27-93d6-3f73c40ae149",
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "import sys\n",
    "sys.path.append('..')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "81c4a29a",
   "metadata": {},
   "source": [
    "## 一，准备数据"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "18d1cca2",
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "dfdata.shape =  (581012, 13)\n",
      "target_col =  Cover_Type\n",
      "cat_cols =  ['Wilderness_Area', 'Soil_Type']\n",
      "num_cols =  ['Elevation', 'Aspect', 'Slope', '...']\n",
      "len(dftrain) =  371847\n",
      "len(dfval) =  92962\n",
      "len(dftest) =  116203\n"
     ]
    }
   ],
   "source": [
    "import numpy as np \n",
    "import pandas as pd \n",
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "file_path = \"covertype.parquet\"\n",
    "dfdata = pd.read_parquet(file_path)\n",
    "\n",
    "cat_cols = ['Wilderness_Area', 'Soil_Type']\n",
    "num_cols = ['Elevation','Aspect','Slope','Horizontal_Distance_To_Hydrology',\n",
    " 'Vertical_Distance_To_Hydrology','Horizontal_Distance_To_Roadways',\n",
    " 'Hillshade_9am','Hillshade_Noon','Hillshade_3pm','Horizontal_Distance_To_Fire_Points']\n",
    "target_col = 'Cover_Type'\n",
    "\n",
    "print(\"dfdata.shape = \",dfdata.shape)\n",
    "print(\"target_col = \", target_col)\n",
    "print('cat_cols = ', cat_cols)  \n",
    "print('num_cols = ', num_cols[:3]+['...'])\n",
    "\n",
    "dftmp, dftest_raw = train_test_split(dfdata, random_state=42, test_size=0.2)\n",
    "dftrain_raw, dfval_raw = train_test_split(dftmp, random_state=42, test_size=0.2)\n",
    "\n",
    "print(\"len(dftrain) = \",len(dftrain_raw))\n",
    "print(\"len(dfval) = \",len(dfval_raw))\n",
    "print(\"len(dftest) = \",len(dftest_raw))\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "169703c2",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "3d974b6e58f34a01adb2829200624f1e",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "  0%|          | 0/2 [00:00<?, ?it/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from torchkeras.tabular import TabularPreprocessor\n",
    "from sklearn.preprocessing import OrdinalEncoder\n",
    "\n",
    "#特征工程\n",
    "pipe = TabularPreprocessor(cat_features = cat_cols, \n",
    "                           embedding_features=cat_cols)\n",
    "encoder = OrdinalEncoder()\n",
    "\n",
    "dftrain = pipe.fit_transform(dftrain_raw.drop(target_col,axis=1))\n",
    "dftrain[target_col] = encoder.fit_transform(\n",
    "    dftrain_raw[target_col].values.reshape(-1,1)).astype(np.int32)\n",
    "\n",
    "dfval = pipe.transform(dfval_raw.drop(target_col,axis=1))\n",
    "dfval[target_col] = encoder.transform(\n",
    "    dfval_raw[target_col].values.reshape(-1,1)).astype(np.int32)\n",
    "\n",
    "dftest = pipe.transform(dftest_raw.drop(target_col,axis=1))\n",
    "dftest[target_col] = encoder.transform(\n",
    "    dftest_raw[target_col].values.reshape(-1,1)).astype(np.int32)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "7768b968",
   "metadata": {},
   "outputs": [],
   "source": [
    "from torchkeras.tabular import TabularDataset\n",
    "from torch.utils.data import Dataset,DataLoader \n",
    "\n",
    "def get_dataset(dfdata):\n",
    "    return TabularDataset(\n",
    "                data = dfdata,\n",
    "                task = 'multiclass', #regression, binary, multiclass\n",
    "                target = [target_col],\n",
    "                continuous_cols = pipe.get_numeric_features(),\n",
    "                categorical_cols = pipe.get_embedding_features()\n",
    "        )\n",
    "\n",
    "def get_dataloader(ds,batch_size=1024,num_workers=0,shuffle=False):\n",
    "    dl = DataLoader(\n",
    "            ds,\n",
    "            batch_size=batch_size,\n",
    "            shuffle=shuffle,\n",
    "            num_workers=num_workers,\n",
    "            pin_memory=False,\n",
    "        )\n",
    "    return dl \n",
    "    \n",
    "ds_train = get_dataset(dftrain)\n",
    "ds_val = get_dataset(dfval)\n",
    "ds_test = get_dataset(dftest)\n",
    "\n",
    "dl_train = get_dataloader(ds_train,shuffle=True)\n",
    "dl_val = get_dataloader(ds_val,shuffle=False)\n",
    "dl_test = get_dataloader(ds_test,shuffle=False)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "ec009434",
   "metadata": {},
   "outputs": [],
   "source": [
    "for batch in dl_train:\n",
    "    break\n",
    "    "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bb2f0170",
   "metadata": {},
   "source": [
    "## 二，定义模型"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "767468ab",
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "32\n",
      "7\n"
     ]
    }
   ],
   "source": [
    "from torchkeras.tabular.models import FTTransformerConfig,FTTransformerModel\n",
    "\n",
    "model_config = FTTransformerConfig(\n",
    "    task=\"multiclass\",  #regression, binary, multiclass\n",
    "    num_attn_blocks=3\n",
    ")\n",
    "\n",
    "config = model_config.merge_dataset_config(ds_train)\n",
    "net = FTTransformerModel(config = config)\n",
    "\n",
    "#初始化参数\n",
    "net.reset_weights()\n",
    "net.data_aware_initialization(dl_train)\n",
    "\n",
    "print(net.backbone.output_dim)\n",
    "print(net.hparams.output_dim)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "7e312c76",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "tensor(1.8597, grad_fn=<AddBackward0>)\n"
     ]
    }
   ],
   "source": [
    "output = net.forward(batch)\n",
    "loss = net.compute_loss(output,batch['target'])\n",
    "print(loss)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "22001eaf",
   "metadata": {},
   "source": [
    "## 三，训练模型"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "14bb01e8",
   "metadata": {},
   "outputs": [],
   "source": [
    "from torchkeras import KerasModel \n",
    "from torchkeras.tabular import StepRunner \n",
    "KerasModel.StepRunner = StepRunner \n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "780167c1",
   "metadata": {},
   "outputs": [],
   "source": [
    "import torch \n",
    "from torch import nn \n",
    "class Accuracy(nn.Module):\n",
    "    def __init__(self):\n",
    "        super().__init__()\n",
    "\n",
    "        self.correct = nn.Parameter(torch.tensor(0.0),requires_grad=False)\n",
    "        self.total = nn.Parameter(torch.tensor(0.0),requires_grad=False)\n",
    "\n",
    "    def forward(self, preds: torch.Tensor, targets: torch.Tensor):\n",
    "        preds = preds.argmax(dim=-1)\n",
    "        targets = targets.reshape(-1)\n",
    "        m = (preds == targets).sum()\n",
    "        n = targets.shape[0] \n",
    "        self.correct += m \n",
    "        self.total += n\n",
    "        \n",
    "        return m/n\n",
    "\n",
    "    def compute(self):\n",
    "        return self.correct.float() / self.total \n",
    "    \n",
    "    def reset(self):\n",
    "        self.correct -= self.correct\n",
    "        self.total -= self.total\n",
    "        "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "d3c69f48",
   "metadata": {},
   "outputs": [],
   "source": [
    "keras_model = KerasModel(net,\n",
    "                   loss_fn=None,\n",
    "                   optimizer = torch.optim.AdamW(net.parameters(),lr = 1e-3),\n",
    "                   metrics_dict = {\"acc\":Accuracy()}\n",
    "                   )\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "08f4ad31",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\u001b[0;31m<<<<<< 🚀 mps is used >>>>>>\u001b[0m\n"
     ]
    },
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAikAAAGJCAYAAABPZ6NtAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAACAPElEQVR4nO3deVxU1fvA8c+AzADKDgIqioq55b6Qu6aJS5a5m+aSaeaSaZuWu9/CFk0r0/r+tM0Ws2j5amlKauaeZuWG4r4AroCCbDPn98eVkZFtQGAGfN6v17xg7j333nOZgXk45zzn6JRSCiGEEEIIO+Ng6woIIYQQQuREghQhhBBC2CUJUoQQQghhlyRIEUIIIYRdkiBFCCGEEHZJghQhhBBC2CUJUoQQQghhlyRIEUIIIYRdkiBFCCGEEHZJghRh92bPno1Op+Py5cu2rkqJOXXqFDqdjk8++cTWVRHFZNy4cTz00EO2rkaJGjFiBBUqVLB1NSwMGjSIAQMG2LoaIhcSpAiRi9dff50ffvjB1tUos7Zv307btm1xdXUlICCAZ599lhs3blh1bFxcHCNHjqRixYq4uLjQtGlTVq9ena1cVFQUkydPpnXr1jg7O6PT6Th16lSO51y1ahVDhw6lVq1a6HQ6OnbsmGO5PXv2MGHCBOrXr0/58uWpWrUqAwYM4OjRo9beOidPnuT//u//eOWVV6wqn5SUhNFotPr8ZZHJZOKTTz7hkUceISgoiPLly3P//ffzn//8h5SUlByPWb58OXXr1sXZ2ZlatWrx3nvvZSvz8ssv89133/H3338X9y2IQpAgRYhcSJBSfPbv30/nzp1JTk5m4cKFPPXUU3z00Uf0798/32MTExNp27Yt3333HU8//TRvv/02bm5uDBgwgC+//NKi7I4dO3j33Xe5fv06devWzfO8S5cu5ccffyQoKAgvL69cy73xxht89913dO7cmcWLFzNmzBh+//13mjZtyoEDB6y6/8WLF1O9enU6deqUa5kNGzbQt29fvLy8qFChAgaDgVq1ajFt2jRiY2Otuk5ZkpyczMiRI7l06RJjx45l0aJFtGzZklmzZtG9e3fuXIbuww8/5KmnnqJ+/fq89957tGrVimeffZY33njDolyTJk1o3rw5CxYsKMnbEdZSQti5WbNmKUBdunSpRK9bvnx5NXz48BK9ZqaTJ08qQH388cc2uX5x6969uwoMDFQJCQnmbf/9738VoNavX5/nsW+++aYCVGRkpHmb0WhULVq0UAEBASo1NdW8/cqVKyoxMVEppdRbb72lAHXy5Mkcz3vmzBllNBqVUkrVr19fdejQIcdy27Zts7iGUkodPXpUGQwGNWTIkDzrrpRSaWlpytfXV02fPj3H/Tdu3FB9+/ZVOp1Ode/eXb333ntqzZo16ptvvlEzZ85UtWrVUp6enurbb7/N91r2Zvjw4ap8+fKFOjY1NVVt27Yt2/Y5c+YoQG3YsMG8LTk5Wfn4+KiePXtalB0yZIgqX768unr1qsX2t99+W5UvX15dv369UHUTxUdaUkSpcfnyZQYMGIC7uzs+Pj5MmjQpx2belStX0qxZM1xcXPD29mbQoEGcPXvWosyxY8fo27cvAQEBODs7U6VKFQYNGkRCQgIAOp2OpKQkPv30U3Q6HTqdjhEjRuRYr7i4OMqVK8ecOXOy7YuKikKn0/H+++8DcPXqVV544QUaNGhAhQoVcHd3p3v37kXS1JyWlsbMmTNp1qwZHh4elC9fnnbt2rFp06ZsZU0mE4sXL6ZBgwY4Ozvj5+dHt27d+PPPPy3KrVy5kpYtW+Lq6oqXlxft27fn119/vat6JiYmsmHDBoYOHYq7u7t5+7Bhw6hQoQLffPNNnsdv3boVPz8/HnzwQfM2BwcHBgwYQGxsLFu2bDFv9/b2xs3Nzap6BQUF4eCQ/5/E1q1bo9frLbbVqlWL+vXrc/jw4XyP/+OPP7h8+TJdunTJti8jI4OHH36YPXv2sGvXLn7++WcmTJhAz5496d+/P3PmzOHQoUNMmzaNxx9/nLVr12Y7x5EjR+jXrx/e3t44OzvTvHlzfvrpJ4syn3zyCTqdjt9//52nn34aHx8f3N3dGTZsGNeuXct2zg8++ID69etjMBioVKkS48ePJz4+Plu5Xbt20aNHD7y8vChfvjwNGzZk8eLF2cqdP3+e3r17U6FCBfz8/HjhhRfy7c7S6/W0bt062/bHHnsMwOJnv2nTJq5cucK4ceMsyo4fP56kpKRsP7eHHnqIpKQkNmzYkGcdRMmTIEWUGgMGDCAlJYXw8HB69OjBu+++y5gxYyzKvPbaawwbNoxatWqxcOFCnnvuOSIjI2nfvr35j2paWhphYWHs3LmTiRMnsmTJEsaMGcOJEyfMZT7//HMMBgPt2rXj888/5/PPP+fpp5/OsV7+/v506NAhxw/XVatW4ejoaO7GOHHiBD/88AMPP/wwCxcu5MUXX+Tff/+lQ4cOXLhw4a5+PomJifzf//0fHTt25I033mD27NlcunSJsLAw9u/fb1F21KhRPPfccwQFBfHGG28wdepUnJ2d2blzp7nMnDlzeOKJJ3BycmLu3LnMmTOHoKAgfvvtN3OZGzducPny5XwfmcEfwL///ktGRgbNmze3qJNer6dx48b89ddfed5namoqLi4u2ba7uroCsHfvXqt/ZkVFKUVcXBy+vr75lt2+fTs6nY4mTZpk2xceHk5UVBQ7d+6kRYsWgBZQJiUlmb+Pj4/npZdeYtGiRTz55JNcv37dfPzBgwd54IEHOHz4MFOnTmXBggWUL1+e3r178/3332e73oQJEzh8+DCzZ89m2LBhfPHFF/Tu3dui62T27NmMHz+eSpUqsWDBAvr27cuHH35I165dSU9PN5fbsGED7du359ChQ0yaNIkFCxbQqVMn1qxZY3FNo9FIWFgYPj4+vP3223To0IEFCxbw0Ucf5fuzy0lm11fWn33me+jO91izZs1wcHDI9h6rV68eLi4ubNu2rVB1EMXI1k05QuQns7vnkUcesdg+btw4Bai///5bKaXUqVOnlKOjo3rttdcsyv3777+qXLly5u1//fWXAtTq1avzvG5Buns+/PBDBah///3XYnu9evXUgw8+aH6ekpJi7lLIdPLkSWUwGNTcuXMttlHA7p6MjIxs3RDXrl1T/v7+6sknnzRv++233xSgnn322WznMJlMSimljh07phwcHNRjjz2Wrb6ZZZTSmu+BfB9Zu05Wr16tAPX7779nu37//v1VQEBAnvc5ceJE5eDgoE6dOmWxfdCgQQpQEyZMyPG4/Lp7ssqruycnn3/+uQLU8uXL8y07dOhQ5ePjk217QkKCcnd3Vz/88IN520cffaS8vLwUoOrXr6++++47lfXPdtOmTdVHH31kft65c2fVoEEDlZKSYt5mMplU69atVa1atczbPv74YwWoZs2aqbS0NPP2zK60H3/8USml1MWLF5Ver1ddu3a1eB+8//77ClArVqxQSmnvverVq6tq1aqpa9euWdxXTu+XrO91pZRq0qSJatasWd4/uFx06dJFubu7W1x3/PjxytHRMcfyfn5+atCgQdm2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0UcW3YJLly4lIyPD/DvVpUsX9Ho97777rsXrvnz5chISEujZsycATZs2pXr16ixatCjb70xOXVuFdfjwYXr27ElwcDBr1qzJtWvwwQcfxNvbm6VLl1psX7p0Ka6uruZ6Zzp06BApKSk5pjgL25KWFFFqnDx5kkceeYRu3bqxY8cOVq5cyeOPP06jRo0A7b+5//znP0ybNo1Tp07Ru3dv3NzcOHnyJN9//z1jxozhhRde4LfffmPChAn079+f++67j4yMDD7//HMcHR3p27ev+XrNmjVj48aNLFy4kEqVKlG9enVCQ0PzrOPAgQMZOnQoH3zwAWFhYdkGET788MPMnTuXkSNH0rp1a/7991+++OILi/9SC+vhhx8mIiKCxx57jJ49e3Ly5EmWLVtGvXr1LKab79SpE0888QTvvvsux44do1u3bphMJrZu3UqnTp2YMGECISEhvPrqq8ybN4927drRp08fDAYDe/bsoVKlSoSHhwOFG5MCWqp469at6dChA2PGjOHcuXMsWLCArl270q1bN3O53bt306lTJ2bNmsXs2bPN2+vVq0f//v2pWrUqJ0+eZOnSpXh7e2dr+k9ISDAHs5nppe+//z6enp54enoyYcIEc9nff/+d33//HdA+0JOSksytau3bt6d9+/YAPP/88/z000/06tWLq1evsnLlSotrZgYNuWnbti0+Pj5s3LjRYq6Xbt26MXbsWP766y+aNGlCr169ePrpp5kwYQITJkzA1dWVOXPm8OKLL9KxY0f69etHRESExQDkJUuW0LZtWxo0aMDo0aOpUaMGcXFx7Nixg3PnzmWbjyctLY3OnTszYMAAoqKi+OCDD2jbtq25RcHPz49p06YxZ84cunXrxiOPPGIu16JFC/O9Ojg4sHTpUnr16kXjxo0ZOXIkgYGBHDlyhIMHD7J+/fo8fybWuH79OmFhYVy7do0XX3wx21wnNWvWNAeMLi4uzJs3j/Hjx9O/f3/CwsLYunUrK1eu5LXXXsvWgrhhwwZcXV3vubWUSgUbZBQJUSCZKciHDh1S/fr1U25ubsrLy0tNmDBB3bx5M1v57777TrVt21aVL19elS9fXtWpU0eNHz9eRUVFKaWUOnHihHryySdVzZo1lbOzs/L29ladOnVSGzdutDjPkSNHVPv27ZWLi4sCrEpHTkxMNJdfuXJltv0pKSnq+eef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//79Fr+Lqampud5ffu9ve7m/69evqxdeeEHt2LFDnTx5Um3cuFE1bdpU1apVS6WkpOR6f/by+uX33lRKqYSEBOXq6qqWLl2a4zns9bWz5jNg7NixqmrVquq3335Tf/75p2rVqpVq1aqVxXlq166tIiIizM+t+X0tLAlS7iFAjo+PP/7YXKZDhw5q+PDh5ufPPfecqlq1qtLr9crf31/16NFD7du3r+Qrb4WBAweqwMBApdfrVeXKldXAgQNVdHS0ef+d96aUUt9884267777lF6vV/Xr11dr164t4VoXzPr16xWgoqKisu0rTa/dpk2bcnwvZtbfZDKpGTNmKH9/f2UwGFTnzp2z3XO1atXUrFmzLLa999575ntu2bKl2rlzZwndkaW87u/kyZO5/i5u2rTJfI477y+/93dJyuv+kpOTVdeuXZWfn59ycnJS1apVU6NHj84WbNjr65ffe1MppT788EPl4uKi4uPjczyHvb521nwG3Lx5U40bN055eXkpV1dX9dhjj6mYmJhs58l6jDW/r4Wlu3VBIYQQQgi7ImNShBBCCGGXJEgRQgghhF2SIEUIIYQQdkmCFCGEEELYJQlShBBCCGGXJEgRQgghhF2SIEUIIYQQdkmCFCGEEELYJQlShBDils2bN6PT6bItsCaEsA0JUoQQQghhlyRIEUIIIYRdkiBFCGE3TCYT4eHhVK9eHRcXFxo1asS3334L3O6KWbt2LQ0bNsTZ2ZkHHniAAwcOWJzju+++o379+hgMBoKDg1mwYIHF/tTUVF5++WWCgoIwGAyEhISwfPlyizJ79+6lefPmuLq60rp1a6Kioor3xoUQOZIgRQhhN8LDw/nss89YtmwZBw8eZPLkyQwdOpQtW7aYy7z44ossWLCAPXv24OfnR69evUhPTwe04GLAgAEMGjSIf//9l9mzZzNjxgw++eQT8/HDhg3jq6++4t133+Xw4cN8+OGHVKhQwaIer776KgsWLODPP/+kXLlyPPnkkyVy/0KIOxTJWspCCHGXUlJSlKurq9q+fbvF9lGjRqnBgwerTZs2KUB9/fXX5n1XrlxRLi4uatWqVUoppR5//HH10EMPWRz/4osvqnr16imllIqKilKA2rBhQ451yLzGxo0bzdvWrl2rAHXz5s0iuU8hhPWkJUUIYReio6NJTk7moYceokKFCubHZ599xvHjx83lWrVqZf7e29ub2rVrc/jwYQAOHz5MmzZtLM7bpk0bjh07htFoZP/+/Tg6OtKhQ4c869KwYUPz94GBgQBcvHjxru9RCFEw5WxdASGEALhx4wYAa9eupXLlyhb7DAaDRaBSWC4uLlaVc3JyMn+v0+kAbbyMEKJkSUuKEMIu1KtXD4PBwJkzZwgJCbF4BAUFmcvt3LnT/P21a9c4evQodevWBaBu3bps27bN4rzbtm3jvvvuw9HRkQYNGmAymSzGuAgh7Je0pAgh7IKbmxsvvPACkydPxmQy0bZtWxISEti2bRvu7u5Uq1YNgLlz5+Lj44O/vz+vvvoqvr6+9O7dG4Dnn3+eFi1aMG/ePAYOHMiOHTt4//33+eCDDwAIDg5m+PDhPPnkk7z77rs0atSI06dPc/HiRQYMGGCrWxdC5EKCFCGE3Zg3bx5+fn6Eh4dz4sQJPD09adq0Ka+88oq5u2X+/PlMmjSJY8eO0bhxY/73v/+h1+sBaNq0Kd988w0zZ85k3rx5BAYGMnfuXEaMGGG+xtKlS3nllVcYN24cV65coWrVqrzyyiu2uF0hRD50Sill60oIIUR+Nm/eTKdOnbh27Rqenp62ro4QogTImBQhhBBC2CUJUoQQQghhl6S7RwghhBB2SVpShBBCCGGXJEgRQgghhF2SIEUIIYQQdkmCFCGEEELYJQlShBBCCGGXJEgRQgghhF2SIEUIIYQQdkmCFCGEEELYpf8H+gRMi27IbXMAAAAASUVORK5CYII=",
      "text/plain": [
       "<Figure size 600x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "\n",
       "<style>\n",
       "    /* background: */\n",
       "    progress::-webkit-progress-bar {background-color: #CDCDCD; width: 100%;}\n",
       "    progress {background-color: #CDCDCD;}\n",
       "\n",
       "    /* value: */\n",
       "    progress::-webkit-progress-value {background-color: #00BFFF  !important;}\n",
       "    progress::-moz-progress-bar {background-color: #00BFFF  !important;}\n",
       "    progress {color: #00BFFF ;}\n",
       "\n",
       "    /* optional */\n",
       "    .progress-bar-interrupted, .progress-bar-interrupted::-webkit-progress-bar {\n",
       "        background: #000000;\n",
       "    }\n",
       "</style>\n"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "\n",
       "    <div>\n",
       "      <progress value='20' class='' max='20' style='width:300px; height:20px; vertical-align: middle;'></progress>\n",
       "      100% [20/20] [11:13]\n",
       "      <br>\n",
       "      ████████████████████100.00% [91/91] [val_loss=0.2189, val_acc=0.9112]\n",
       "    </div>\n",
       "    "
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>epoch</th>\n",
       "      <th>train_loss</th>\n",
       "      <th>train_acc</th>\n",
       "      <th>lr</th>\n",
       "      <th>val_loss</th>\n",
       "      <th>val_acc</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>1</td>\n",
       "      <td>0.647109</td>\n",
       "      <td>0.729593</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.535932</td>\n",
       "      <td>0.769454</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>2</td>\n",
       "      <td>0.509820</td>\n",
       "      <td>0.782478</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.430417</td>\n",
       "      <td>0.820561</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>3</td>\n",
       "      <td>0.450258</td>\n",
       "      <td>0.809892</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.376553</td>\n",
       "      <td>0.842333</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>4</td>\n",
       "      <td>0.414448</td>\n",
       "      <td>0.825743</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.343716</td>\n",
       "      <td>0.859609</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>5</td>\n",
       "      <td>0.388642</td>\n",
       "      <td>0.837272</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.324666</td>\n",
       "      <td>0.866203</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>6</td>\n",
       "      <td>0.369072</td>\n",
       "      <td>0.845816</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.302208</td>\n",
       "      <td>0.875982</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>7</td>\n",
       "      <td>0.353593</td>\n",
       "      <td>0.853539</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.299520</td>\n",
       "      <td>0.876466</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>8</td>\n",
       "      <td>0.343755</td>\n",
       "      <td>0.857441</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.286678</td>\n",
       "      <td>0.881382</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>9</td>\n",
       "      <td>0.334802</td>\n",
       "      <td>0.861491</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.271402</td>\n",
       "      <td>0.889869</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>10</td>\n",
       "      <td>0.322605</td>\n",
       "      <td>0.866838</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.266368</td>\n",
       "      <td>0.891386</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>11</td>\n",
       "      <td>0.314963</td>\n",
       "      <td>0.870124</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.255356</td>\n",
       "      <td>0.895301</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>11</th>\n",
       "      <td>12</td>\n",
       "      <td>0.309985</td>\n",
       "      <td>0.871859</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.254360</td>\n",
       "      <td>0.895409</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>12</th>\n",
       "      <td>13</td>\n",
       "      <td>0.304366</td>\n",
       "      <td>0.874085</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.245309</td>\n",
       "      <td>0.899421</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>13</th>\n",
       "      <td>14</td>\n",
       "      <td>0.296666</td>\n",
       "      <td>0.877154</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.241898</td>\n",
       "      <td>0.901497</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>14</th>\n",
       "      <td>15</td>\n",
       "      <td>0.294303</td>\n",
       "      <td>0.878157</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.232252</td>\n",
       "      <td>0.905047</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>15</th>\n",
       "      <td>16</td>\n",
       "      <td>0.289774</td>\n",
       "      <td>0.880483</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.229934</td>\n",
       "      <td>0.906489</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>16</th>\n",
       "      <td>17</td>\n",
       "      <td>0.285711</td>\n",
       "      <td>0.882543</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.226360</td>\n",
       "      <td>0.907887</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>17</th>\n",
       "      <td>18</td>\n",
       "      <td>0.280952</td>\n",
       "      <td>0.884221</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.223477</td>\n",
       "      <td>0.909425</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>18</th>\n",
       "      <td>19</td>\n",
       "      <td>0.278261</td>\n",
       "      <td>0.885407</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.224185</td>\n",
       "      <td>0.908264</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>19</th>\n",
       "      <td>20</td>\n",
       "      <td>0.274726</td>\n",
       "      <td>0.887187</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.218908</td>\n",
       "      <td>0.911233</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "    epoch  train_loss  train_acc     lr  val_loss   val_acc\n",
       "0       1    0.647109   0.729593  0.001  0.535932  0.769454\n",
       "1       2    0.509820   0.782478  0.001  0.430417  0.820561\n",
       "2       3    0.450258   0.809892  0.001  0.376553  0.842333\n",
       "3       4    0.414448   0.825743  0.001  0.343716  0.859609\n",
       "4       5    0.388642   0.837272  0.001  0.324666  0.866203\n",
       "5       6    0.369072   0.845816  0.001  0.302208  0.875982\n",
       "6       7    0.353593   0.853539  0.001  0.299520  0.876466\n",
       "7       8    0.343755   0.857441  0.001  0.286678  0.881382\n",
       "8       9    0.334802   0.861491  0.001  0.271402  0.889869\n",
       "9      10    0.322605   0.866838  0.001  0.266368  0.891386\n",
       "10     11    0.314963   0.870124  0.001  0.255356  0.895301\n",
       "11     12    0.309985   0.871859  0.001  0.254360  0.895409\n",
       "12     13    0.304366   0.874085  0.001  0.245309  0.899421\n",
       "13     14    0.296666   0.877154  0.001  0.241898  0.901497\n",
       "14     15    0.294303   0.878157  0.001  0.232252  0.905047\n",
       "15     16    0.289774   0.880483  0.001  0.229934  0.906489\n",
       "16     17    0.285711   0.882543  0.001  0.226360  0.907887\n",
       "17     18    0.280952   0.884221  0.001  0.223477  0.909425\n",
       "18     19    0.278261   0.885407  0.001  0.224185  0.908264\n",
       "19     20    0.274726   0.887187  0.001  0.218908  0.911233"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "keras_model.fit(\n",
    "    train_data = dl_train,\n",
    "    val_data= dl_val,\n",
    "    ckpt_path='checkpoint',\n",
    "    epochs=20,\n",
    "    patience=10,\n",
    "    monitor=\"val_acc\", \n",
    "    mode=\"max\",\n",
    "    plot = True,\n",
    "    wandb = False\n",
    ")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "092528f3",
   "metadata": {},
   "source": [
    "## 四，评估模型"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "15693dba",
   "metadata": {},
   "outputs": [],
   "source": [
    "keras_model.evaluate(dl_train)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4f52f49a",
   "metadata": {},
   "outputs": [],
   "source": [
    "keras_model.evaluate(dl_val)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6050f49a",
   "metadata": {},
   "outputs": [],
   "source": [
    "keras_model.evaluate(dl_test)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ced7f651",
   "metadata": {},
   "source": [
    "## 五，使用模型"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0c3f0dd5",
   "metadata": {},
   "outputs": [],
   "source": [
    "from tqdm import tqdm \n",
    "net = net.cpu()\n",
    "net.eval()\n",
    "preds = []\n",
    "with torch.no_grad():\n",
    "    for batch in tqdm(dl_test):\n",
    "        preds.append(net.predict(batch))\n",
    "    "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e330d9f7",
   "metadata": {},
   "outputs": [],
   "source": [
    "yhat_list = [yd.argmax(dim=-1).tolist() for yd in preds]\n",
    "yhat = []\n",
    "for yd in yhat_list:\n",
    "    yhat.extend(yd)\n",
    "yhat = encoder.inverse_transform(np.array(yhat).reshape(-1,1))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ac0b9cad",
   "metadata": {},
   "outputs": [],
   "source": [
    "dftest_raw = dftest_raw.rename(columns = {target_col: 'y'})\n",
    "dftest_raw['yhat'] = yhat"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7cd623c7",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.metrics import classification_report\n",
    "print(classification_report(y_true = dftest_raw['y'],y_pred = dftest_raw['yhat']))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "75b5f2ee",
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import pandas as pd\n",
    "import matplotlib.pyplot as plt\n",
    "import seaborn as sns\n",
    "from sklearn.metrics import confusion_matrix\n",
    "\n",
    "\n",
    "# 计算混淆矩阵\n",
    "cm = confusion_matrix(dftest_raw['y'], dftest_raw['yhat'])\n",
    "\n",
    "# 将混淆矩阵转换为DataFrame\n",
    "df_cm = pd.DataFrame(cm, index=['Actual {}'.format(i) for i in range(cm.shape[0])],\n",
    "                     columns=['Predicted {}'.format(i) for i in range(cm.shape[1])])\n",
    "\n",
    "# 使用seaborn绘制混淆矩阵\n",
    "plt.figure(figsize=(10,7))\n",
    "sns.heatmap(df_cm, annot=True, fmt='d', cmap='Blues', cbar=False)\n",
    "plt.title('Confusion Matrix')\n",
    "plt.xlabel('Predicted labels')\n",
    "plt.ylabel('True labels')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8bd458bc",
   "metadata": {},
   "source": [
    "## 六，保存模型"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "84fd68e5",
   "metadata": {},
   "source": [
    "最佳模型权重已经保存在ckpt_path = 'checkpoint'位置了。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "039d1fd4",
   "metadata": {},
   "outputs": [],
   "source": [
    "net.load_state_dict(torch.load('checkpoint',weights_only=True))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6af119bc",
   "metadata": {},
   "source": [
    "## 七，与LightGBM对比"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5a217aa4",
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "import pandas as pd \n",
    "import lightgbm as lgb\n",
    "from sklearn.preprocessing import OrdinalEncoder\n",
    "from sklearn.metrics import accuracy_score \n",
    "\n",
    "\n",
    "dftmp, dftest_raw = train_test_split(dfdata, random_state=42, test_size=0.2)\n",
    "dftrain_raw, dfval_raw = train_test_split(dftmp, random_state=42, test_size=0.2)\n",
    "\n",
    "dftrain = dftrain_raw.copy()\n",
    "dfval = dfval_raw.copy()\n",
    "dftest = dftest_raw.copy()\n",
    "\n",
    "target_col = 'Cover_Type'\n",
    "cat_cols = ['Wilderness_Area', 'Soil_Type']\n",
    "\n",
    "encoder = OrdinalEncoder()\n",
    "\n",
    "dftrain[target_col] = encoder.fit_transform(dftrain[target_col].values.reshape(-1,1)) \n",
    "dfval[target_col] = encoder.transform(dfval[target_col].values.reshape(-1,1))\n",
    "dftest[target_col] = encoder.transform(dftest[target_col].values.reshape(-1,1))\n",
    "\n",
    "for col in cat_cols:\n",
    "    dftrain[col] = dftrain[col].astype(int)\n",
    "    dfval[col] = dfval[col].astype(int)\n",
    "    dftest[col] = dftest[col].astype(int)\n",
    "\n",
    "ds_train = lgb.Dataset(dftrain.drop(columns=[target_col]), label=dftrain[target_col],categorical_feature=cat_cols)\n",
    "ds_val = lgb.Dataset(dfval.drop(columns=[target_col]), label=dfval[target_col],categorical_feature=cat_cols)\n",
    "ds_test = lgb.Dataset(dftest.drop(columns=[target_col]), label=dftest[target_col],categorical_feature=cat_cols)\n",
    "\n",
    "\n",
    "import lightgbm as lgb\n",
    "\n",
    "params = {\n",
    "    'n_estimators':500,\n",
    "    'boosting_type': 'gbdt',\n",
    "    'objective':'multiclass',\n",
    "    'num_class': 7,  # 类别数量\n",
    "    'metric': 'multi_logloss', \n",
    "    'learning_rate': 0.01,\n",
    "    'verbose': 1,\n",
    "    'early_stopping_round':50\n",
    "}\n",
    "model = lgb.train(params, ds_train, \n",
    "        valid_sets=[ds_val], \n",
    "        valid_names=['validate']\n",
    "        )\n",
    "\n",
    "y_pred_val = model.predict(dfval.drop(target_col,axis = 1), num_iteration=model.best_iteration)\n",
    "y_pred_val = np.argmax(y_pred_val, axis=1)\n",
    "\n",
    "y_pred_test = model.predict(dftest.drop(target_col,axis = 1), num_iteration=model.best_iteration)\n",
    "y_pred_test = np.argmax(y_pred_test, axis=1)\n",
    "\n",
    "val_score = accuracy_score(dfval[target_col], y_pred_val)\n",
    "test_score = accuracy_score(dftest[target_col], y_pred_test) \n",
    "\n",
    "print('val_score = ',val_score)\n",
    "print('test_score = ' , test_score)"
   ]
  }
 ],
 "metadata": {
  "kaggle": {
   "accelerator": "gpu",
   "dataSources": [],
   "dockerImageVersionId": 30747,
   "isGpuEnabled": true,
   "isInternetEnabled": true,
   "language": "python",
   "sourceType": "notebook"
  },
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.14"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
